๐ ๋น๊ด์ ๋ฝ vs ๋๊ด์ ๋ฝ + MyBatis ์ ์ฉ ๊ฐ์ด๋
NOTE
๋ฉํฐ ์๋ฒ/ํธ๋์ญ์ ํ๊ฒฝ์ ๋์์ฑ ์ ์ด โ ๋น๊ด์ ๋ฝ๊ณผ ๋๊ด์ ๋ฝ์ ๊ฐ๋ ยทMyBatis ๊ตฌํยท์ฌ์๋ ๋ก์งยท์ ํ ๊ธฐ์ค.
๐ ๊ฐ๋
1. ์ DB ๋ฝ์ด ํ์ํ๊ฐ?
๋ฉํฐ ์๋ฒ / ๋ฉํฐ ํธ๋์ญ์ ํ๊ฒฝ์์๋ ๋ค์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค.
- ์ฌ๊ณ ์ค๋ณต ์ฐจ๊ฐ
- ํฌ์ธํธ ์ด์ค ์ฌ์ฉ
- ๊ณ์ข ์์ก ์ค์ผ
- ๋ง์ง๋ง ์์ ๋ฎ์ด์ฐ๊ธฐ (Lost Update)
Java Lock์ ๋จ์ผ JVM์์๋ง ์ ํจ
๐ ์ด์ ํ๊ฒฝ์์๋ DB ๋ ๋ฒจ ์ ์ด ํ์
2. ๋น๊ด์ ๋ฝ (Pessimistic Lock)
๐ ๊ฐ๋
โ์ถฉ๋์ ๋ฐ๋์ ๋ฐ์ํ๋คโ๋ผ๊ณ ๊ฐ์ . ์ฝ๋ ์๊ฐ๋ถํฐ ๋ฝ์ ๊ฑด๋ค.
SQL
SELECT * FROM product
WHERE id = 1
FOR UPDATE;- ํด๋น row์ Exclusive Lock
- ํธ๋์ญ์ ์ข ๋ฃ๊น์ง ์ ์ง
MyBatis ์ ์ฉ
<select id="selectForUpdate" resultType="Product">
SELECT *
FROM product
WHERE id = #{id}
FOR UPDATE
</select>Service ๋ ๋ฒจ (์ค์)
@Transactional
public void decreaseStock(Long productId, int quantity) {
Product product = productMapper.selectForUpdate(productId);
if (product.getStock() < quantity) {
throw new IllegalStateException("์ฌ๊ณ ๋ถ์กฑ");
}
productMapper.updateStock(productId, quantity);
}โ ๏ธ ๋ฐ๋์ ๊ธฐ์ตํ ๊ฒ
- @Transactional ํ์
- ํธ๋์ญ์ ๋ฒ์๊ฐ ๊ธธ๋ฉด ์ฑ๋ฅ ์ ํ
- ๋ฐ๋๋ฝ ๊ฐ๋ฅ์ฑ ์กด์ฌ
3. ๋๊ด์ ๋ฝ (Optimistic Lock)
๐ข ๊ฐ๋
โ์ถฉ๋์ ๊ฑฐ์ ์๋คโ๋ผ๊ณ ๊ฐ์ . DB์ ๋ฌผ๋ฆฌ์ ๋ฝ์ ๊ฑธ์ง ์๋๋ค. ๋์ version ๊ฐ์ ๋น๊ตํ๋ค.
ํ ์ด๋ธ ๊ตฌ์กฐ
id | stock | version4. MyBatis์์ ๋๊ด์ ๋ฝ ๊ตฌํ ๋ฐฉ๋ฒ โญ (ํต์ฌ)
MyBatis๋ JPA์ฒ๋ผ @Version ์๋ ์ฒ๋ฆฌ ๊ธฐ๋ฅ์ด ์๋ค.
๐ ์ง์ ๊ตฌํํด์ผ ํ๋ค.
4.1 SELECT ์ version ์กฐํ
<select id="selectById" resultType="Product">
SELECT id, stock, version
FROM product
WHERE id = #{id}
</select>4.2 UPDATE ์ version ๋น๊ต
<update id="updateWithVersion">
UPDATE product
SET stock = #{stock},
version = version + 1
WHERE id = #{id}
AND version = #{version}
</update>๐ ํต์ฌ: WHERE ์ ์ version ํฌํจ
4.3 Service ๋ ๋ฒจ ๊ตฌํ (์ค๋ฌด ํจํด)
@Transactional
public void decreaseStock(Long productId, int quantity) {
Product product = productMapper.selectById(productId);
if (product.getStock() < quantity) {
throw new IllegalStateException("์ฌ๊ณ ๋ถ์กฑ");
}
int updatedRows = productMapper.updateWithVersion(
productId,
product.getStock() - quantity,
product.getVersion()
);
if (updatedRows == 0) {
throw new IllegalStateException("๋์์ฑ ์ถฉ๋ ๋ฐ์");
}
}4.4 ์ updatedRows๋ฅผ ํ์ธํด์ผ ํ๋๊ฐ?
์ถฉ๋ ๋ฐ์ โ version ๊ฐ์ด ๋ฌ๋ผ์ง
โ UPDATE ์ํฅ row = 0
โ ์คํจ ๊ฐ์ง ๊ฐ๋ฅ์ด ์ฒดํฌ๋ฅผ ์ ํ๋ฉด ๐ ๋๊ด์ ๋ฝ์ด ๋ฌด์๋ฏธํด์ง๋ค.
5. ์ฌ์๋ ๋ก์ง (์ค๋ฌด์์ ๊ฑฐ์ ํ์)
public void decreaseWithRetry(Long productId, int quantity) {
int retry = 3;
while (retry-- > 0) {
try {
decreaseStock(productId, quantity);
return;
} catch (IllegalStateException e) {
if (!e.getMessage().contains("๋์์ฑ")) {
throw e;
}
}
}
throw new IllegalStateException("์ฌ์๋ ์คํจ");
}6. ๋น๊ด์ vs ๋๊ด์ ๋น๊ต
| ๊ตฌ๋ถ | ๋น๊ด์ ๋ฝ | ๋๊ด์ ๋ฝ |
|---|---|---|
| ๋ฝ ์์ | SELECT ์ | UPDATE ์ |
| DB ๋ฌผ๋ฆฌ์ ๋ฝ | ์์ | ์์ |
| ์ฑ๋ฅ | ๋ฎ์ | ๋์ |
| ๋ฐ๋๋ฝ | ๊ฐ๋ฅ | ์์ |
| ์ฌ์๋ | ํ์ ์์ | ํ์ |
7. ์ค๋ฌด ์ ํ ๊ธฐ์ค
๐ฅ ์ฌ๊ณ ์ฐจ๊ฐ / ๊ธ์ต ๐ ๋น๊ด์ ๋ฝ
- ์ ๋ ํ๋ฆฌ๋ฉด ์ ๋จ
- ๊ฐํ ์ ํฉ์ฑ ํ์
๐ ๊ฒ์๊ธ ์์ / ์ผ๋ฐ CRUD ๐ ๋๊ด์ ๋ฝ
- ์ถฉ๋ ํ๋ฅ ๋ฎ์
- ์ฑ๋ฅ ์ค์
8. MyBatis ํ๊ฒฝ์์ ์์ฃผ ํ๋ ์ค์
- โ version ์ปฌ๋ผ ์ ๋ง๋ฆ
- โ WHERE ์ ์ version ์ ๋ฃ์
- โ update row count ์ฒดํฌ ์ ํจ
- โ ์ฌ์๋ ๋ก์ง ์์
- โ ํธ๋์ญ์ ๋ฒ์ ๊ณผ๋
9. ๊ตฌ์กฐ์ ์ผ๋ก ์ดํดํ๊ธฐ
๋น๊ด์ ๋ฝ = ๋จผ์ ์ ๊ทธ๊ณ ์์
๋๊ด์ ๋ฝ = ๋์ค์ ๋น๊ตํ๊ณ ํ๋จ๐ ๋ฉด์ ์ฉ ์ ๋ฆฌ ๋ฉํธ
MyBatis์์๋ ๋๊ด์ ๋ฝ์ ์๋ ์ง์ํ์ง ์๊ธฐ ๋๋ฌธ์ version ์ปฌ๋ผ์ ๋๊ณ UPDATE ์ version์ ๋น๊ตํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํฉ๋๋ค. UPDATE ๊ฒฐ๊ณผ row count๊ฐ 0์ด๋ฉด ์ถฉ๋๋ก ํ๋จํ๊ณ ์ฌ์๋ ๋ก์ง์ ์ค๊ณํฉ๋๋ค.
๐ฏ ์ต์ข ํต์ฌ ์์ฝ
- MyBatis๋ ๋๊ด์ ๋ฝ ์๋ ๊ตฌํ
- UPDATE ์ version ๋น๊ต ํ์
- ์ํฅ row ์ ํ์ธ ํ์
- ์ฌ์๋ ๋ก์ง ์ค๊ณ ์ค์
- ๊ฐํ ์ ํฉ์ฑ = ๋น๊ด์
- ๊ณ ์ฑ๋ฅ = ๋๊ด์